package jdbmaplite.mapper.impl;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import jdbmaplite.manager.DBManager;
import jdbmaplite.manager.ParseConfigException;
import jdbmaplite.mapper.DBDao;
import jdbmaplite.mapper.SqlDefinition;
import jdbmaplite.mapper.SqlDefinitionFactory;
import jdbmaplite.util.ArrayUtils;
import jdbmaplite.util.ClassUtils;
import jdbmaplite.util.DBUtils;
import jdbmaplite.util.LogUtils;
import jdbmaplite.util.MethodInfo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JDBMapLiteDao implements DBDao {

	private static final Log _cat = LogFactory.getLog(JDBMapLiteDao.class);

	private DBManager dbManager = null;
	private SqlDefinitionFactory sqlDefinitionFactory = null;

	/**
	 * Default constructor.
	 * 
	 * @param dbManager
	 *            DBManager
	 * @param configFilePath
	 *            String
	 * @param isAbsolutePath
	 *            boolean
	 * @throws ParseConfigException
	 */
	public JDBMapLiteDao(DBManager dbManager, String configFilePath,
			boolean isAbsolutePath) throws ParseConfigException {
		super();
		this.dbManager = dbManager;
		this.sqlDefinitionFactory = new JDBMapLiteSqlDefinitionFactory(
				configFilePath, isAbsolutePath);
	}

	/**
	 * Default constructor.
	 * 
	 * @param dbManager
	 *            DBManager
	 * @throws ParseConfigException
	 */
	public JDBMapLiteDao(DBManager dbManager) throws ParseConfigException {
		super();
		this.dbManager = dbManager;
		String configFilePath = ClassUtils.toClassResourcePath(getClass())
				.concat(dbManager.getDaoSuffixFileName()).concat(".xml");
		this.sqlDefinitionFactory = new JDBMapLiteSqlDefinitionFactory(
				configFilePath, false);
	}

	public Connection getConnection() throws SQLException {
		return dbManager.getConnectionFactory().getConnection();
	}

	public void commitQuietly(Connection conn) {
		DBUtils.commitQuietly(conn);
	}

	public void closeQuietly(Connection conn) {
		DBUtils.closeQuietly(conn);
	}

	public void rollbackQuietly(Connection conn) {
		DBUtils.rollbackQuietly(conn);
	}

	public void rollbackQuietly(Connection conn, Savepoint savepoint) {
		DBUtils.rollbackQuietly(conn, savepoint);
	}

	/**
	 * Execute Update
	 * 
	 * @param parameters
	 *            Object...
	 * @throws SQLException
	 */
	public void executeUpdate(Object... parameters) throws SQLException {
		String actionName = "Execute update";
		Connection conn = (parameters.length > 0
				&& parameters[0] instanceof Connection ? (Connection) parameters[0]
				: null);
		boolean isLocalTrans = (conn == null);
		String sql = null;
		try {
			// prepare data
			int shiftTraceCount = (isLocalTrans ? 0 : 1);
			MethodInfo methodInfo = sqlDefinitionFactory.getMethodInfo(
					this.getClass(), shiftTraceCount, parameters);
			SqlDefinition sqlDef = sqlDefinitionFactory
					.getSqlDefinition(methodInfo.getSimpleMethodName());
			sql = sqlDefinitionFactory.formatSql(sqlDef,
					methodInfo.getParameterNames(), parameters);
			if (_cat.isDebugEnabled()) {
				_cat.debug(LogUtils.formatSqlLog(actionName, sql));
			}
			// start transition
			if (isLocalTrans) {
				conn = getConnection();
			}
			Statement stmt = conn.createStatement();
			stmt.executeUpdate(sql);
		} catch (SQLException e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw e;
		} catch (Throwable e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw new SQLException(actionName);
		} finally {
			if (isLocalTrans) {
				closeQuietly(conn);
			}
		}
	}

	/**
	 * Execute Update
	 * 
	 * @param conn
	 *            Connection
	 * @param parameters
	 *            Object...
	 * @throws SQLException
	 */
	public void executeUpdate(Connection conn, Object... parameters)
			throws SQLException {
		// move connection args into the first position of parameters
		parameters = ArrayUtils.unshift(parameters, conn);
		executeUpdate(parameters);
	}

	/**
	 * Execute Object Update
	 * 
	 * @param conn
	 *            Connection
	 * @param parameterObject
	 *            Object
	 * @throws SQLException
	 */
	public void executeObjectUpdate(Connection conn, Object parameterObject)
			throws SQLException {
		String actionName = "Execute object update";
		boolean isLocalTrans = (conn == null);
		String sql = null;
		try {
			// prepare data
			int shiftTraceCount = (isLocalTrans ? 1 : 0);
			MethodInfo methodInfo = sqlDefinitionFactory.getMethodInfo(
					this.getClass(), shiftTraceCount, parameterObject);
			SqlDefinition sqlDef = sqlDefinitionFactory
					.getSqlDefinition(methodInfo.getSimpleMethodName());
			sql = sqlDefinitionFactory.formatSqlByObject(sqlDef,
					parameterObject);
			if (_cat.isDebugEnabled()) {
				_cat.debug(LogUtils.formatSqlLog(actionName, sql));
			}
			// start transition
			if (isLocalTrans) {
				conn = getConnection();
			}
			Statement stmt = conn.createStatement();
			stmt.executeUpdate(sql);
		} catch (SQLException e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw e;
		} catch (Throwable e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw new SQLException(actionName);
		} finally {
			if (isLocalTrans) {
				closeQuietly(conn);
			}
		}
	}

	/**
	 * Execute Object Update
	 * 
	 * @param parameterObject
	 *            Object
	 * @throws SQLException
	 */
	public void executeObjectUpdate(Object parameterObject) throws SQLException {
		executeObjectUpdate(null, parameterObject);
	}

	/**
	 * Execute Query
	 * 
	 * @param parameters
	 *            Object...
	 * @return T
	 * @throws SQLException
	 */
	public <T> T executeQuery(Object... parameters) throws SQLException {
		String actionName = "Execute query";
		Connection conn = (parameters.length > 0
				&& parameters[0] instanceof Connection ? (Connection) parameters[0]
				: null);
		boolean isLocalTrans = (conn == null);
		String sql = null;
		try {
			// prepare data
			int shiftTraceCount = (isLocalTrans ? 0 : 1);
			MethodInfo methodInfo = sqlDefinitionFactory.getMethodInfo(
					this.getClass(), shiftTraceCount, parameters);
			SqlDefinition sqlDef = sqlDefinitionFactory
					.getSqlDefinition(methodInfo.getSimpleMethodName());
			Class<T> type = methodInfo.getCastType();
			Field[] fields = methodInfo.loadCastTypeDeclaredFields(false);
			int fieldSize = (fields != null ? fields.length : 0);
			sql = sqlDefinitionFactory.formatSql(sqlDef,
					methodInfo.getParameterNames(), parameters);
			if (_cat.isDebugEnabled()) {
				_cat.debug(LogUtils.formatSqlLog(actionName, sql));
			}
			// start transition
			if (isLocalTrans) {
				conn = getConnection();
			}
			Statement stmt = conn.createStatement();
			ResultSet result = stmt.executeQuery(sql);
			if (result.next()) {
				T object = type.newInstance();
				for (int i = 0; i < fieldSize; i++) {
					Field field = fields[i];
					field.setAccessible(true);
					field.set(object, sqlDefinitionFactory.convertResultValue(
							result, i + 1, field.getType()));
				}
				return object;
			}
		} catch (SQLException e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw e;
		} catch (Throwable e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw new SQLException(actionName);
		} finally {
			if (isLocalTrans) {
				closeQuietly(conn);
			}
		}
		return null;
	}

	/**
	 * Execute Query
	 * 
	 * @param conn
	 *            Connection
	 * @param parameters
	 *            Object...
	 * @return T
	 * @throws SQLException
	 */
	@SuppressWarnings("unchecked")
	public <T> T executeQuery(Connection conn, Object... parameters)
			throws SQLException {
		// move connection args into the first position of parameters
		parameters = ArrayUtils.unshift(parameters, conn);
		// sun jdk's generic bug, cast to the T is necessary
		return (T) executeQuery(parameters);
	}

	/**
	 * Execute Bulk Query
	 * 
	 * @param parameters
	 *            Object...
	 * @return List{T}
	 * @throws SQLException
	 */
	public <T> List<T> executeBulkQuery(Object... parameters)
			throws SQLException {
		String actionName = "Execute bulk query";
		Connection conn = (parameters.length > 0
				&& parameters[0] instanceof Connection ? (Connection) parameters[0]
				: null);
		boolean isLocalTrans = (conn == null);
		String sql = null;
		List<T> ret = new ArrayList<T>();
		try {
			// prepare data
			int shiftTraceCount = (isLocalTrans ? 0 : 1);
			MethodInfo methodInfo = sqlDefinitionFactory.getMethodInfo(
					this.getClass(), shiftTraceCount, parameters);
			SqlDefinition sqlDef = sqlDefinitionFactory
					.getSqlDefinition(methodInfo.getSimpleMethodName());
			Class<T> type = methodInfo.getCastType();
			Field[] fields = methodInfo.loadCastTypeDeclaredFields(false);
			int fieldSize = (fields != null ? fields.length : 0);
			sql = sqlDefinitionFactory.formatSql(sqlDef,
					methodInfo.getParameterNames(), parameters);
			if (_cat.isDebugEnabled()) {
				_cat.debug(LogUtils.formatSqlLog(actionName, sql));
			}
			// start transition
			if (isLocalTrans) {
				conn = getConnection();
			}
			Statement stmt = conn.createStatement();
			ResultSet result = stmt.executeQuery(sql);
			while (result.next()) {
				T object = type.newInstance();
				for (int i = 0; i < fieldSize; i++) {
					Field field = fields[i];
					field.setAccessible(true);
					field.set(object, sqlDefinitionFactory.convertResultValue(
							result, i + 1, field.getType()));
				}
				ret.add(object);
			}
		} catch (SQLException e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw e;
		} catch (Throwable e) {
			_cat.warn(LogUtils.formatSqlLog(actionName, sql), e);
			throw new SQLException(actionName);
		} finally {
			if (isLocalTrans) {
				closeQuietly(conn);
			}
		}
		return ret;
	}

	/**
	 * Execute Bulk Query
	 * 
	 * @param conn
	 *            Connection
	 * @param parameters
	 *            Object...
	 * @return List{T}
	 * @throws SQLException
	 */
	public <T> List<T> executeBulkQuery(Connection conn, Object... parameters)
			throws SQLException {
		// move connection args into the first position of parameters
		parameters = ArrayUtils.unshift(parameters, conn);
		return executeBulkQuery(parameters);
	}

	public void reloadSqlDefinition() throws ParseConfigException {
		sqlDefinitionFactory.reloadSqlDefinition();
	}

	/**
	 * Get DbManager
	 * 
	 * @return DBManager
	 */
	public DBManager getDbManager() {
		return dbManager;
	}

	/**
	 * Set DbManager
	 * 
	 * @param dbManager
	 *            DBManager
	 */
	public void setDbManager(DBManager dbManager) {
		this.dbManager = dbManager;
	}

	/**
	 * Get SqlDefinitionFactory
	 * 
	 * @return SqlDefinitionFactory
	 */
	public SqlDefinitionFactory getSqlDefinitionFactory() {
		return sqlDefinitionFactory;
	}

	/**
	 * Set SqlDefinitionFactory
	 * 
	 * @param sqlDefinitionFactory
	 *            SqlDefinitionFactory
	 */
	public void setSqlDefinitionFactory(
			SqlDefinitionFactory sqlDefinitionFactory) {
		this.sqlDefinitionFactory = sqlDefinitionFactory;
	}

}